feat(providers): LLM/ASR 供应商改成渠道卡片——同一家可存多把 key,排序即优先级 - #918
Conversation
原来一个供应商只能存一份配置,换 key 只能把旧的覆盖掉;想在主号/备号之间来回切, 每次都要重新粘贴。这个 PR 把「服务 → AI 提供商」从「下拉选厂商 + 一组字段」改成 一张张可命名、可排序、可开关的渠道卡片。 心智只有一条:**排序即优先级,列表里第一个启用的就是当前生效的渠道**。关掉的渠道 自动沉到末尾;后端不另存「当前选中」,避免「列表第一张是 A、实际请求打的是 B」 这种两处真相。 本 PR 只做多渠道的存储、编辑与切换,**不含失败重试与故障转移**(那是下一个 PR, 需要先把听写/润色链路里几十处隐式读 active 凭据的地方显式化)。 存储与迁移 - ChannelMeta(providerType / order / enabled / lastTest)以 serde(flatten) 嵌进 ASR、LLM 两个 entry;v1 老 payload 照常反序列化。 - providerType 与渠道 id 解耦:同一家厂商的多张卡片各有自己的 id,但 providerType 都指向同一个厂商实现。coordinator::resolve_effective_asr_provider 和 commands 里 几十处 `== PROVIDER_ID` 的比较依赖它,拿成 id 会让整个 ASR 路由失效。 - v1→v2 迁移幂等:迁移出的渠道 **id 沿用原 preset id**,不生成 uuid,老用户的 map key 一个字节都不变,重复执行结果一致。 - 迁移只在内存里做、不主动落盘:启动时写 keyring 会在 macOS 触发钥匙串 ACL 弹窗, 留给下一次真实写入顺带固化。 - clean_credentials 原本会 retain(!is_empty) 删掉空 entry —— 刚点「添加渠道」、名字 取好了还没填 key 的卡片会被静默删掉,改为「渠道卡片只能由用户显式删除」。 - ChannelMeta 手写 Default(derive 会让 enabled=false,而 write_account 用 entry().or_default() 建 entry,新写入的渠道会一出生就被禁用)。 - Windows 全新安装预置一张 Foundry 本地 ASR 卡片,保住开箱即用。 IPC - 新增 list/create/rename/delete/set_enabled/reorder/record_test 七个命令。 - set_credential/read_credential 的 provider 作用域原本硬性拒绝 LLM 账户,补上 get/set_for_llm_provider —— 编辑列表里第 3 张 LLM 卡片必须能按 id 定位。 - validate_provider_credentials / list_provider_models 新增可选 channel_id: 卡片上的「测试连通」要测用户点的那一张,而不是当前生效的那张。 前端 - ChannelList:卡片列表、拖拽排序、开关沉底、生效中/失败标红/延迟展示。 - 添加与编辑弹窗;本地引擎与 Codex OAuth 不做预置固定卡片,和云端厂商一样从 「添加渠道」里选,只是编辑时没有 key/地址字段。 - 新手引导(Onboarding)列表为空时直接摊开添加表单,不让新用户对着空列表发呆。 - 五种语言文案齐备。 验证:cargo test --lib persistence::credentials(31 passed)、commands::(104 passed)、 npm test(含 tsc + vite build,退出码 0)、浏览器 mock 预览逐项走查四种卡片状态与两个弹窗。 设计与分期见 docs/provider-channels-plan.md。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
三处都是装机自用后暴露的问题。
**添加渠道少一步。** 原来是「先建卡片、再填凭据」两步 —— 那只是实现上需要先有渠道 id
才能写凭据(凭据按 id 作用域存),不该变成用户多点一次。改成点+ 直接开一个完整弹窗,
供应商、名字、密钥、地址、模型、连接检查都在一屏里;草稿卡片在后台先建出来,用户什么
都没填就关掉时由 delete_channel_if_blank 回收,不会在列表里留空卡片。换供应商也不再
需要重建卡片,走新增的 set_channel_provider_type。
**拖拽在打包后的 app 里根本不动。** Tauri 的 webview 默认开着 dragDropEnabled,会把
HTML5 的 dragstart/drop 当成文件拖放吞掉 —— 浏览器预览里是好的,真机里是坏的,只验
前者就会漏。改用 pointer 事件手写,顺带让 Windows / Android 行为一致。
改的过程中炸出第二个:最初用 setPointerCapture,它把后续事件重定向到手柄,浏览器补发
的 click 于是落到设置弹窗的遮罩上(遮罩挂着 onClick={onClose}),**拖一下卡片整个设置
面板就关了**。改成 window 级监听不动事件目标,并在捕获阶段吞掉拖拽后紧跟的那一次 click。
**卡片状态不再假装"健康"。** 原来当前那张有绿点 + 「生效中」文字,两者都被读成"这张
能用",可它只代表排在最前面 —— 一张 key 已经失效的卡片照样排第一。优先级和健康度是两
个正交的维度,被压成了一个视觉。现在:
- 绿点与「生效中」全部去掉,当前那张只用左侧一条竖条表达位置,不带健康暗示;
- 验证按钮移到卡片上(开关左侧),**按钮自己就是结果容器**:未验过显示「验证」、
通过显示延迟数字(`284ms`,数字本身既说明通了又能比快慢)、失败显示能指导行动的短
标签(`✗ 401` 改 key / `✗ 429` 等会儿 / `✗ 超时` 查网络);
- 副行显示上次验证是多久以前,超过一天的结果褪色 —— 让"这条结论会过期"可见;
- 按钮宽度固定,避免文字变化把开关和箭头挤来挤去;
- **不做自动验证**:验证是真实 API 调用(LLM 走一次真润色、ASR 传一段静音音频),
打开设置就全部验一遍等于按卡片数烧额度,还容易撞进限流。
顺带把 mock 的 reorderChannels 改成真重排:原来是空操作,浏览器预览里松手后顺序被
listChannels 拉回原样,看着像"拖拽坏了",会误导下一个人。
另:补上此前遗漏的 rustfmt(自己编辑范围内的行)。
验证:cargo test --lib persistence::credentials(31 passed)、commands::(101 passed)、
tsc 0;浏览器侧用精确的 pointer 事件序列验了拖动跟手、松手持久化、设置面板不被误关。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`active` 指向一个已不存在的 entry 是真实会发生的:前端 prefs 里的 activeAsrProvider 与凭据库里的 active.asr 是两份数据,历史上可能不同步。原来遇到这种情况纯按字母序挑一张 排第一,完全不看那张有没有填过 key —— 结果很容易把一张空卡排到最前,用户升级后打开就 看到「未配置」,而他真正配好的那张其实还在列表下面躺着。 排序优先级改为:原 active → 填过凭据的 → 字母序(兜底,保证幂等)。 同时补一个边界测试钉死底线:即使 active 指向缺失 entry,迁移也**绝不碰任何凭据** —— 所有 key 原样留在各自 entry 里,用户把想用的那张拖回第一位就能恢复。 验证:cargo test --lib persistence::credentials(33 passed,跑前已备份 preferences.json 且跑后 md5 未变)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
排查时在代码里搜到 retry 容易误以为渠道重试做了。补两条既有行为的说明: net.rs::send_with_retry 只对连接层失败重连同一个 endpoint(拿到任何 HTTP 响应即返回、 超时不重试),永远不会换卡片;润色失败已经会回落插入 ASR 原文,所以「全渠道失败 → 出原文」这条决策天然满足,P2 要做的是在回落前多试几张卡片。 同时写明 P0 的定位:多渠道现在的价值是存档与手动切换,排第二的卡片不会被自动用上。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
…with Open-Less#924 omni/Open-Less#927 selection-polish/Open-Less#928 local-asr)
- sync_active_channels 全部禁用时清空 active,运行时不再使用已禁用渠道凭据 - create_channel 落盘前 compact_orders,消除新卡与禁用项 order 冲突 - 换供应商时仅空槽写入 preset 默认 endpoint/model(不覆盖用户已填值) - LocalAsr 激活前确保本地引擎渠道卡存在、启用并置顶 - 更新 provider-channels-plan 状态与待确认项
User description
这个 PR 解决什么
一个供应商只能存一份配置,换 key 只能把旧的覆盖掉;想在主号/备号之间来回切,每次都要重新粘贴。
把「服务 → AI 提供商」从「下拉选厂商 + 一组字段」改成一张张可命名、可排序、可开关的渠道卡片。同一家厂商可以有多张卡片、各自一把 key,切换只是把卡片拖到最上面,旧的那把原样留着。
心智只有一条:排序即优先级,列表里第一个启用的就是当前生效的渠道。关掉的渠道自动沉到末尾;后端不另存「当前选中」,避免「列表第一张是 A、实际请求打的是 B」这种两处真相。
设计与分期见
docs/provider-channels-plan.md。范围(重要)
本 PR 只做多渠道的存储、编辑与切换,不含失败重试与故障转移。
现在的行为仍是:一次失败就是一次失败(润色失败照旧回落插入 ASR 原文),排在第二的卡片不会被自动用上。也就是说,多渠道当前的价值是「存档 + 手动切换」,不是「自动容错」。
重试要求「这次请求换用渠道 B」,而听写/润色主链路目前是隐式读全局 active 的(
CredentialsVault::get(...)自己去查active.asr/llm,调用方无法指定渠道)。那几十处的显式化是独立的一步,留给后续 PR,本 PR 主链路一行没动。存储与迁移
ChannelMeta(providerType/order/enabled/lastTest)以serde(flatten)嵌进 ASR、LLM 两个 entry;v1 老 payload 照常反序列化。providerType与渠道 id 解耦:同一家厂商的多张卡片各有自己的 id,但providerType都指向同一个厂商实现。coordinator::resolve_effective_asr_provider与 commands 里几十处== PROVIDER_ID的比较依赖它,拿成 id 会让整个 ASR 路由失效。active指向一个已不存在的 entry 是真实会发生的(前端 prefs 与凭据库里的 active 是两份数据),此时若纯按字母序挑,很容易把一张空卡排到第一,用户升级后就看到「未配置」,而配好的那张还在列表下面躺着。顺手修掉的既有缺陷
这些都不是本次改出来的,是渠道化把它们暴露了:
ChannelMeta若derive(Default),enabled默认为false,而write_account用entry().or_default()建 entryclean_credentials会retain(!is_empty)删掉空 entryset_credential/read_credential的provider作用域硬性拒绝 LLM 账户get/set_for_llm_provider)dragDropEnabled,吞掉 HTML5 的dragstart/dropdraggable在打包后的 app 里完全不触发(浏览器预览里却是好的)。已改用 pointer 事件手写,Windows / Android 行为也一致setPointerCapture会把后续事件重定向到手柄,浏览器补发的 click 落到设置弹窗遮罩上(遮罩挂着onClick={onClose})UI
284ms,既说明通了又能比快慢)、失败显示能指导行动的短标签(✗ 401改 key /✗ 429等会儿 /✗ 超时查网络)。副行显示上次验证多久以前,超过一天褪色 —— 让「这条结论会过期」可见。验证
新增测试覆盖:id 沿用 preset id、迁移幂等(逐字节比对)、迁移后
lookup_account行为不变、迁移绝不碰凭据(即使 active 指向缺失 entry)、优先选已配置渠道、排序/开关边界(关掉沉底、重开落到启用组末尾、order 压实、前端漏报 id 不撞车)、新建空卡片不被clean_credentials删掉、v1 payload 兼容、同厂商多卡片的providerType独立。已在 macOS 上装机自用:老配置迁移后 6 张 ASR 卡片一张没丢,实际在用的那张正确排在第一位,模型名按渠道正确隔离。
已 rebase 到含 #915(ZenMux)的最新 beta,ZenMux 的两处分支(vocabulary note、
AsrAdvancedOptions)已并入新的渠道字段组件。🤖 Generated with Claude Code
PR Type
Enhancement, Tests
Description
Provider configs become reorderable, toggleable channel cards.
Add idempotent in-memory v1→v2 credentials migration.
Active provider derived from first enabled channel.
Add channel management IPC, UI, and translations.
Diagram Walkthrough
File Walkthrough
10 files
Add channel metadata, migration, and managementScope validation and model listing per channelAdd Tauri IPC commands for channel managementRoute credential reads and writes by channel kindAdd channel IPC wrapper with browser mockUse channel list during onboarding with auto-createAdd channel card list UI componentAdapt ASR credentials helpers to channel idsRoute provider settings to channel list pageRefactor providers section to embed channel list1 files
Register new channel commands in invoke handler2 files
Export channels module from commandsExport channel IPC functions from index5 files
Add Japanese channel management translationsAdd Traditional Chinese channel translationsAdd Korean channel management translationsAdd Simplified Chinese channel translationsAdd English channel management translations1 files
Document provider channels design and phasing